<?php
/**
 * OpenID 2.0 Authentication for PHP
 *
 * PHP version 5
 *
 * LICENSE:
 *
 * Copyright (c) 2006-2007 Pádraic Brady <padraic.brady@yahoo.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *    * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *    * The name of the author may not be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @category    Authentication
 * @package     OpenID
 * @author      Pádraic Brady <padraic.brady@yahoo.com>
 * @license     http://opensource.org/licenses/bsd-license.php New BSD License
 * @version     $Id: Authorisation.php 57 2007-07-17 20:03:27Z padraic $
 * @link        http://
 */

/** OpenID_Response_Exception */
require_once 'OpenID/Response/Exception.php';

/** Crypt_HMAC2 */
require_once 'Crypt/HMAC2.php';

/**
 * Container for an Authorisation Response
 *
 * @category   Zend
 * @package    OpenID
 * @copyright  Copyright (c) 2007 Pádraic Brady (http://blog.astrumfutura.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class OpenID_Response_Authorisation
{

    /**
     * Stored parameters for this Response
     *
     * @var array
     */
    protected $_params = array();

    /**
     * Typical namespaces in an authorisation response
     *
     * @var array
     */
    protected $_authResponseNamespaces = array('openid', 'ns');

    /**
     * Stores for later retrieval, the response type. See the list of
     * possible authorisation response Constants defined in OpenID
     *
     * @var string
     */
    protected $_result = false;

    /**
     * Constructor
     *
     * @param array $params
     */
    public function __construct(array $params)
    {
        // Namespaces may also include any available extension namespaces
        // the user set at runtime
        $this->_authResponseNamespaces = array_merge($this->_authResponseNamespaces, array_keys(OpenID::getExtensionNamespaces()));
        $this->_setParams($params);
        $this->_result = $this->_check();
    }

    /**
     * Return the current result type; one of the authorisation
     * response Constant values in OpenID
     *
     * @return string
     */
    public function getResult()
    {
        return $this->_result;
    }

    /**
     * Gets a value from the current namespaces $_SESSION array
     *
     * @param string $key
     * @return mixed
     */
    public function getSessionValue($key)
    {
        if (isset($_SESSION[OpenID::OPENID_PHP_SESSION_NAMESPACE][$key])) {
            return $_SESSION[OpenID::OPENID_PHP_SESSION_NAMESPACE][$key];
        }
        return null;
    }

    /**
     * Based on an array of values, constructs the Key:Value form of all key
     * names (i.e. period delimited key spacing), e.g. "openid_sreg_email"
     * becomes "openid.sreg.email".
     * Key:Value forms are used to construct a KV string which is the data
     * the response signature was based upon, and which we must recalculate
     * using the DiffieHellman secret to verify the message.
     *
     * @param array $params
     * @return void
     */
    public function _setParams(array $params)
    {
        $namespaces = $this->_authResponseNamespaces;
        $malformedKeys = array_keys($params);
        $arguments = array();
        foreach($malformedKeys as $key) {
            $fixed = $key;
            if (preg_match("/^openid/", $key)) {
                foreach($namespaces as $namespace) {
                    if (preg_match("/{$namespace}_/", $key)) {
                        $fixed = preg_replace("/{$namespace}_/", $namespace . '.', $fixed);
                    }
                }
                if ($fixed != $key) {
                    $value = $params[$key];
                    unset($params[$key]);
                    $arguments[$fixed] = $value;
                }
            }
        }

        $this->_params = $arguments;
    }

    /**
     * Return the array of parameters
     *
     * @return array
     */
    public function getParams()
    {
        if (empty($this->_params)) {
            throw new OpenID_Response_Exception('No query string values were found');
        }
        return $this->_params;
    }

    /**
     * Get a specific parameter from the response
     *
     * @param string $key
     * @return string
     */
    public function get($key)
    {
        if (isset($this->_params[$key])) {
            return $this->_params[$key];
        }
        return null;
    }

    /**
     * Perform a sequence of validation checks on the response data including
     * signature matching to ensure the encrypted data was returned from a
     * Provider server which we have agreed a Diffie-Hellman shared key with
     *
     * @return
     */
    protected function _check()
    {
        $keys = explode(',', $this->_params['openid.signed']);

        // get the Hash algorithm which applies to this response (used by HMAC)
        $hashAlgorithm = OpenID::getHashFromAssociation($this->_params['openid.assoc_handle']);

        // check all signed keys actually exist in the response
        $signedKeys = array();
        foreach($keys as $key) {
            $fixed = $key;
            if (preg_match("/^openid/", $key)) {
                if (preg_match("/{$namespace}_/", $key)) {
                    $fixed = preg_replace("/{$namespace}_/", $namespace . '.', $fixed);
                }
            }
            $signedKeys[] = $fixed;
        }
        foreach($signedKeys as $key) {
            if (!in_array($key, $keys)) {
                return OpenID::OPENID_RESPONSE_PARSE_ERROR;
            }
        }

        // construct the KV form of the response
        $signedData = '';
        foreach($signedKeys as $key) {
            $signedData .= $key . ':' . $this->_params['openid.' . $key] . "\n";
        }

        // check the signature
        $sharedSecret = base64_decode($this->getSessionValue('shared_secret'));
        if ($this->getSessionValue('shared_secret')) {
            $sharedSecret = base64_decode($this->getSessionValue('shared_secret'));
        } elseif($this->getSessionValue('mac_key')) { // resulted from an unencrypted association
            $sharedSecret = base64_decode($this->getSessionValue('mac_key'));
        } else {
           return OpenID::OPENID_RESPONSE_PARSE_ERROR;
        }
        $opSignature = base64_decode($this->_params['openid.sig']);
        $hmac = new Crypt_HMAC2($sharedSecret, $hashAlgorithm);
        $thisSignature = $hmac->hash($signedData, Crypt_HMAC2::BINARY);
        if ($thisSignature !== $opSignature) {
            return OpenID::OPENID_RESPONSE_PARSE_ERROR;
        }

        // check namespaces are returned and valid
        if (isset($this->_params['openid.ns'])) {
            if (in_array($this->_params['openid.ns'], array(OpenID::OPENID_1_1_NAMESPACE, OpenID::OPENID_1_0_NAMESPACE))) {
                OpenID::setVersion(1.1);
            } elseif ($this->_params['openid.ns'] !== OpenID::OPENID_2_0_NAMESPACE) {
                return OpenID::OPENID_RESPONSE_PARSE_ERROR;
            }
        }

        // check mode values
        if (isset($this->_params['openid.mode'])) {
            if ($this->_params['openid.mode'] == 'setup_needed') {
                return OpenID::OPENID_RESPONSE_SETUP_NEEDED;
            } elseif ($this->_params['openid.mode'] == 'cancel') {
                return OpenID::OPENID_RESPONSE_CANCEL;
            } elseif ($this->_params['openid.mode'] !== 'id_res') {
                return OpenID::OPENID_RESPONSE_PARSE_ERROR;
            }
        }

        // check validity of the nonce
        if (strlen($this->_params['openid.response_nonce']) >= 255 || !preg_match("/^\d{4}-\d{2}-\d{2}T{1}\d{2}:\d{2}:\d{2}Z[0-9A-Za-z]{1,234}$/", $this->_params['openid.response_nonce'])) {
            return OpenID::OPENID_RESPONSE_PARSE_ERROR;
        } else {
            // check we haven't previously received this nonce from the OP
                // otherwise this request is suspect and should be discarded
        }

        return OpenID::OPENID_RESPONSE_SUCCESS;
    }

}